<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2009-2020 Vanilla Forums Inc.
 * @license GPL-2.0-only
 */

namespace VanillaTests\Library\Vanilla\Utility;

use Garden\Schema\Invalid;
use Garden\Schema\Schema;
use Garden\Schema\Validation;
use Garden\Schema\ValidationException;
use Garden\Schema\ValidationField;
use PHPUnit\Framework\TestCase;
use Vanilla\Utility\SchemaUtils;

/**
 * Tests for the `SchemaUtils` class.
 */
class SchemaUtilsTest extends TestCase
{
    /**
     * Test some only one of error cases.
     *
     * @param array $properties
     * @param int $count
     * @dataProvider provideOnlyOneOfErrors
     */
    public function testOnlyOneOfErrors(array $properties, int $count = 1)
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::onlyOneOf($properties, $count);

        $value = $fn(new \ArrayObject(["a" => 1, "b" => 1, "c" => 1]), $field);
        $this->assertSame(Invalid::value(), $value);
        $this->assertFalse($field->isValid());
        $this->assertStringContainsString(implode(", ", $properties), $field->getValidation()->getMessage());
    }

    /**
     * Provide some only one of errors.
     *
     * @return array
     */
    public function provideOnlyOneOfErrors(): array
    {
        $r = [
            "a, b" => [["a", "b"]],
            "a, b, c" => [["a", "b", "c"], 2],
        ];
        return $r;
    }

    /**
     * Test some only one of happy paths.
     *
     * @param array $properties
     * @param int $count
     * @dataProvider provideOnlyOneOfHappy
     */
    public function testOnlyOneOfHappy(array $properties, int $count = 1)
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::onlyOneOf($properties, $count);

        $value = $fn(["a" => 1, "b" => 1], $field);
        $this->assertSame(["a" => 1, "b" => 1], $value);
        $this->assertTrue($field->isValid());
    }

    /**
     * Provide some only one of errors.
     *
     * @return array
     */
    public function provideOnlyOneOfHappy(): array
    {
        $r = [
            "a, c" => [["a", "c"]],
            "a, b, c" => [["a", "b", "c"], 2],
        ];
        return $r;
    }

    /**
     * The only one of validator should only validate arrayish values.
     */
    public function testOnlyOneOfNotArray(): void
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::onlyOneOf(["a", "b"]);

        $value = $fn("foo", $field);
        $this->assertSame("foo", $value);
    }

    /**
     * @return void
     */
    public function testOnlyTogetherHappy(): void
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::onlyTogether(["a", "b", "c"]);

        $fn(["a" => 1, "b" => 2, "c" => 3], $field);
        $this->assertTrue($field->isValid());
    }

    /**
     * @return void
     */
    public function testOnlyTogetherInvalid(): void
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::onlyTogether(["a", "b", "c"]);

        $fn(["a" => 1, "b" => 2], $field);
        $this->assertFalse($field->isValid());
    }

    /**
     * @return void
     */
    public function testFieldRequirementValid(): void
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::fieldRequirement("a", "b");

        $fn(["a" => 1, "b" => 2], $field);
        $this->assertTrue($field->isValid());
    }

    /**
     * @return void
     */
    public function testFieldRequirementInvalid(): void
    {
        $field = new ValidationField(new Validation(), [], "");
        $fn = SchemaUtils::fieldRequirement("a", "b");

        $fn(["a" => 1], $field);
        $this->assertFalse($field->isValid());
    }

    /**
     * Validating an array should work with valid data.
     */
    public function testValidateArrayValid(): void
    {
        $schema = Schema::parse([":i"]);
        $arr = ["1", "2"];
        SchemaUtils::validateArray($arr, $schema);
        $this->assertSame([1, 2], $arr);
    }

    /**
     * The validate array should throw an exception when the value isn't an array.
     */
    public function testValidateArrayNotArray(): void
    {
        $schema = Schema::parse([":i"]);
        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage("is not a valid integer");
        $arr = "a";
        SchemaUtils::validateArray($arr, $schema);
    }

    /**
     * Validating an array should throw an exception for single items.
     */
    public function testValidateInvalidItem(): void
    {
        $schema = Schema::parse([":i"]);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage("is not a valid integer");
        $arr = [1, "a"];
        SchemaUtils::validateArray($arr, $schema);
    }

    /**
     * Test the flattenSchema() method.
     *
     * @param Schema $input
     * @param Schema $expected
     * @dataProvider provideFlatten
     */
    public function testFlatten(Schema $input, Schema $expected)
    {
        $actual = SchemaUtils::flattenSchema($input, "/");
        $this->assertEquals($expected->getSchemaArray(), $actual->getSchemaArray());
    }

    /**
     * @return iterable
     */
    public function provideFlatten(): iterable
    {
        $alreadyFlatSchema = Schema::parse(["key1:s"]);
        yield "already flat" => [$alreadyFlatSchema, $alreadyFlatSchema];

        yield "flatten nested objects" => [
            Schema::parse(["flat:s", "nested" => Schema::parse(["level2:s"])]),
            Schema::parse(["flat:s", "nested/level2:s"]),
        ];

        yield "optional items" => [
            Schema::parse([
                "flat:s?",
                "nestedOuterInnerOptional?" => Schema::parse(["level2:s?"]),
                "nestedOuterRequiredInnerOptional" => Schema::parse(["level2:s?"]),
                "nestedOuterOptionalInnerRequired?" => Schema::parse(["level2:s"]),
            ]),
            Schema::parse([
                "flat:s?",
                "nestedOuterInnerOptional/level2:s?",
                "nestedOuterRequiredInnerOptional/level2:s?",
                "nestedOuterOptionalInnerRequired/level2:s?" => [
                    "minLength" => 1,
                ],
            ])->setField("required", []),
        ];

        yield "arrays" => [
            Schema::parse([
                "flat:s",
                "nested" => Schema::parse([
                    "arr:a" => Schema::parse(["arrItem:s"]),
                ]),
            ]),
            Schema::parse(["flat:s", "nested/arr:a" => Schema::parse(["arrItem:s"])]),
        ];
    }
}
